package com.illtamer.infinite.derive.path;

import com.illtamer.infinite.derive.Path;
import com.illtamer.infinite.derive.util.Assert;
import lombok.Getter;
import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;

public class FilePath implements Path {

    private static final File[] EMPTY_FILE_LIST = new File[0];

    private final FilePath parent;

    @Getter
    private final File file;

    private final String pathLevel;

    private int pathCacheHash;

    private List<FilePath> pathCache;

    public FilePath(String filePath) {
        this(new File(filePath));
    }

    public FilePath(File file) {
        this("/" + file.getName(), file, null);
    }

    private FilePath(String pathLevel, File file, FilePath parent) {
        Assert.isTrue(file.exists(), "File path must exists");
        this.parent = parent;
        this.file = file;
        this.pathLevel = file.isDirectory() && pathLevel.length() != 1 ? pathLevel + '/' : pathLevel;
    }

    @Override
    public @Nullable Path getParent() {
        check();
        return parent;
    }

    @SuppressWarnings("all")
    @Override
    public @NotNull List<Path> list() {
        check();
        refreshPathCache();
        return (List) pathCache;
    }

    @Override
    public @NotNull String getRelativePath() {
        return pathLevel;
    }

    @Override
    public @NotNull String getAbsolutePath() {
        return file.getAbsolutePath();
    }

    @SneakyThrows(FileNotFoundException.class)
    @Override
    public @NotNull InputStream getInputStream() {
        return new FileInputStream(file);
    }

    @Override
    public boolean isDirectory() {
        check();
        return file.isDirectory();
    }

    @Override
    public @NotNull String getName() {
        return file.getName();
    }

    @Override
    public long getSize() {
        return file.length();
    }

    @Override
    public Date lastModified() {
        return new Date(file.lastModified());
    }

    private void refreshPathCache() {
        if (!file.isDirectory()) {
            if (pathCache != null && pathCache.size() == 1 && pathCache.get(0).file.equals(file)) return;
            pathCache = Collections.singletonList(this);
        } else {
            File[] files = Optional.ofNullable(file.listFiles()).orElse(EMPTY_FILE_LIST);
            int h = hashPathCache(files);
            if (h == pathCacheHash) return;
            pathCache = Arrays.stream(files)
                    .map(file -> new FilePath(pathLevel + file.getName(), file, this))
                    .collect(Collectors.toList());
            pathCacheHash = h;
        }
    }

    private void check() {
        Assert.isTrue(file.exists(), "File(%s) was modified", this);
    }

    private static int hashPathCache(File[] files) {
        int h = 0;
        for (File file : files)
            h = h * 31 + file.hashCode();
        return h;
    }

    @Override
    public String toString() {
        return "FilePath(" +
                "parent=" + (parent != null ? parent.pathLevel : null) +
                ", pathLevel=" + pathLevel +
                ", directory=" + file.isDirectory() +
                ')';
    }
}
